///
///
///
import {
useStatusBar,
openInTheSameTab,
encodeTitleURI,
} from "../scrapbox-userscript-std/dom.ts";
import type { Scrapbox, SearchedTitle } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts";
declare const scrapbox: Scrapbox;
const id = "next-action";
let initialized: Promise;
const dummyImage = "/assets/img/favicon/apple-touch-icon.png";
export function setup(projects: string[]) {
const selector = `head style[data-userscript-name="${id}"]`;
document.querySelector(selector)?.remove?.();
const style = document.createElement("style");
style.dataset.userscriptName = id;
style.textContent = `a#${id}.tool-btn:hover {
text-decoration: none;
}
a#${id}.tool-btn::before {
position: absolute;
content: "\\f0ae";
font: 900 20px/46px "Font Awesome 5 Free";
}
a#${id}.tool-btn img {
opacity: 0;
}
a#${id}.tool-btn ~ ul a::before {
position: absolute;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
a#${id}.tool-btn ~ ul img {
opacity: 0;
margin-right: 0;
}`;
document.head.append(style);
if (!document.getElementById(id)) {
scrapbox.PageMenu.addMenu({
title: id,
image: dummyImage,
onClick: async () => {
initialized ??= load(projects);
await initialized;
},
});
}
}
async function load(projects: string[]) {
scrapbox.PageMenu(id).removeAllItems();
const { render, dispose } = useStatusBar();
let count = 0;
try {
for (const project of projects) {
render(
{ type: "spinner" },
{ type: "text", text: `Searching "/${project}" for next actions...`},
);
for await (const title of listNextActions(project)) {
count++;
scrapbox.PageMenu(id).addItem({
title,
onClick: () => {
const path = `https://scrapbox.io/${
project
}/${encodeTitleURI(title)}`;
if (project !== scrapbox.Project.name) {
window.open(path);
return;
}
openInTheSameTab(path);
},
});
}
if (project === projects[projects.length - 1]) continue;
scrapbox.PageMenu(id).addSeparator();
}
render(
{ type: "check-circle" },
{ type: "text", text: `Found ${count} actions.`},
);
} catch(e: unknown) {
render(
{ type: "exclamation-triangle" },
{ type: "text", text: e instanceof Error ?
`${e.name} ${e.message}` :
`Unknown error! (see developper console)`,
},
);
console.error(e);
} finally {
setTimeout(() => dispose(), 1000);
}
}
async function* listNextActions(project: string, filter?: RegExp) {
filter ??= /^(?:⬜(?:[^p]*|p[^-]*))|🔳/;
if (project === scrapbox.Project.name) {
for (const { title, exists } of scrapbox.Project.pages) {
if (!filter.test(title)) continue;
yield title;
}
return;
}
const titles = new Set();
for await (const title of getLinks(project)) {
if (!titles.has(title) && filter.test(title)) {
titles.add(title);
yield title;
}
}
}
async function* getLinks(project: string) {
const path = `/api/pages/${project}/search/titles`;
let followingId = null;
do {
const path_ = `${path}${
followingId ? `?followingId=${followingId}` : ""
}` as string;
const res = await fetch(path_);
followingId = res.headers.get("X-following-id");
const pages = (await res.json()) as SearchedTitle[];
for (const { title, links } of pages) {
yield title;
for (const link of links) {
yield link;
}
}
if (!followingId) break; // 空文字列の場合もある
} while (true)
}